package org.helium.util;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;

/**
 * @author coral
 * @version 创建时间：2015年4月22日
 * 类说明
 */
public class Base32 {
	public static class Alphabet {
		private Alphabet() {
		}

		public static final String BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=";
		public static final String BASE32HEX = "0123456789ABCDEFGHIJKLMNOPQRSTUV=";
	};

	private String alphabet;
	private boolean padding, lowercase;

	/**
	 * Creates an object that can be used to do base32 conversions.
	 * 
	 * @param alphabet
	 *            Which alphabet should be used
	 * @param padding
	 *            Whether padding should be used
	 * @param lowercase
	 *            Whether lowercase characters should be used. default
	 *            parameters (The standard base32 alphabet, no padding,
	 *            uppercase)
	 */
	public Base32(String alphabet, boolean padding, boolean lowercase) {
		this.alphabet = alphabet;
		this.padding = padding;
		this.lowercase = lowercase;
	}

	static private int blockLenToPadding(int blocklen) {
		switch (blocklen) {
		case 1:
			return 6;
		case 2:
			return 4;
		case 3:
			return 3;
		case 4:
			return 1;
		case 5:
			return 0;
		default:
			return -1;
		}
	}

	static private int paddingToBlockLen(int padlen) {
		switch (padlen) {
		case 6:
			return 1;
		case 4:
			return 2;
		case 3:
			return 3;
		case 1:
			return 4;
		case 0:
			return 5;
		default:
			return -1;
		}
	}

	/**
	 * Convert binary data to a base32-encoded String
	 * 
	 * @param b
	 *            An array containing binary data
	 * @return A String containing the encoded data
	 */
	public String toString(byte[] b) {
		ByteArrayOutputStream os = new ByteArrayOutputStream();

		for (int i = 0; i < (b.length + 4) / 5; i++) {
			short s[] = new short[5];
			int t[] = new int[8];

			int blocklen = 5;
			for (int j = 0; j < 5; j++) {
				if ((i * 5 + j) < b.length)
					s[j] = (short) (b[i * 5 + j] & 0xFF);
				else {
					s[j] = 0;
					blocklen--;
				}
			}
			int padlen = blockLenToPadding(blocklen);

			// convert the 5 byte block into 8 characters (values 0-31).

			// upper 5 bits from first byte
			t[0] = (byte) ((s[0] >> 3) & 0x1F);
			// lower 3 bits from 1st byte, upper 2 bits from 2nd.
			t[1] = (byte) (((s[0] & 0x07) << 2) | ((s[1] >> 6) & 0x03));
			// bits 5-1 from 2nd.
			t[2] = (byte) ((s[1] >> 1) & 0x1F);
			// lower 1 bit from 2nd, upper 4 from 3rd
			t[3] = (byte) (((s[1] & 0x01) << 4) | ((s[2] >> 4) & 0x0F));
			// lower 4 from 3rd, upper 1 from 4th.
			t[4] = (byte) (((s[2] & 0x0F) << 1) | ((s[3] >> 7) & 0x01));
			// bits 6-2 from 4th
			t[5] = (byte) ((s[3] >> 2) & 0x1F);
			// lower 2 from 4th, upper 3 from 5th;
			t[6] = (byte) (((s[3] & 0x03) << 3) | ((s[4] >> 5) & 0x07));
			// lower 5 from 5th;
			t[7] = (byte) (s[4] & 0x1F);

			// write out the actual characters.
			for (int j = 0; j < t.length - padlen; j++) {
				char c = alphabet.charAt(t[j]);
				if (lowercase)
					c = Character.toLowerCase(c);
				os.write(c);
			}

			// write out the padding (if any)
			if (padding) {
				for (int j = t.length - padlen; j < t.length; j++)
					os.write('=');
			}
		}

		return new String(os.toByteArray());
	}

	/**
	 * Convert a base32-encoded String to binary data
	 * 
	 * @param str
	 *            A String containing the encoded data
	 * @return An array containing the binary data, or null if the string is
	 *         invalid
	 */
	public byte[] fromString(String str) {
		ByteArrayOutputStream bs = new ByteArrayOutputStream();
		byte[] raw = str.getBytes();
		for (int i = 0; i < raw.length; i++) {
			char c = (char) raw[i];
			if (!Character.isWhitespace(c)) {
				c = Character.toUpperCase(c);
				bs.write((byte) c);
			}
		}

		if (padding) {
			if (bs.size() % 8 != 0)
				return null;
		} else {
			while (bs.size() % 8 != 0)
				bs.write('=');
		}

		byte[] in = bs.toByteArray();

		bs.reset();
		DataOutputStream ds = new DataOutputStream(bs);

		for (int i = 0; i < in.length / 8; i++) {
			short[] s = new short[8];
			int[] t = new int[5];

			int padlen = 8;
			for (int j = 0; j < 8; j++) {
				char c = (char) in[i * 8 + j];
				if (c == '=')
					break;
				s[j] = (short) alphabet.indexOf(in[i * 8 + j]);
				if (s[j] < 0)
					return null;
				padlen--;
			}
			int blocklen = paddingToBlockLen(padlen);
			if (blocklen < 0)
				return null;

			// all 5 bits of 1st, high 3 (of 5) of 2nd
			t[0] = (s[0] << 3) | s[1] >> 2;
			// lower 2 of 2nd, all 5 of 3rd, high 1 of 4th
			t[1] = ((s[1] & 0x03) << 6) | (s[2] << 1) | (s[3] >> 4);
			// lower 4 of 4th, high 4 of 5th
			t[2] = ((s[3] & 0x0F) << 4) | ((s[4] >> 1) & 0x0F);
			// lower 1 of 5th, all 5 of 6th, high 2 of 7th
			t[3] = (s[4] << 7) | (s[5] << 2) | (s[6] >> 3);
			// lower 3 of 7th, all of 8th
			t[4] = ((s[6] & 0x07) << 5) | s[7];

			try {
				for (int j = 0; j < blocklen; j++)
					ds.writeByte((byte) (t[j] & 0xFF));
			} catch (IOException e) {
			}
		}

		return bs.toByteArray();
	}
}
